Add WhenUpdatesCompleted to Foundry MemoryProvider to await async memory extraction - #653
Conversation
e4cc68d to
b43f47a
Compare
This comment has been minimized.
This comment has been minimized.
b43f47a to
eab5828
Compare
This comment has been minimized.
This comment has been minimized.
Foundry extracts memories asynchronously after a run, so store() is fire-and-forget and returns before memories are persisted. Retain the long-running poller returned by BeginUpdateMemories (guarded by a mutex) and expose WhenUpdatesCompleted to await the most recent update. This lets callers and tests observe stored memories before the next search without making the store path block.
eab5828 to
442ca39
Compare
# Conflicts: # provider/foundryprovider/memory_test.go
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by Go API Consistency Review Agent · sonnet46 · 31.2 AIC · ⌖ 5.89 AIC · ⊞ 5.7K
| return nil | ||
| } | ||
| if _, err := poller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: pollingInterval}); err != nil { | ||
| return err |
There was a problem hiding this comment.
Parity gap — superseded terminal state
The .NET WhenUpdatesCompletedAsync explicitly treats MemoryStoreUpdateStatus.Superseded as a successful terminal state (returns without error), mirroring Foundry service semantics where a superseded update is implicitly replaced by a newer one.
The Go implementation delegates to runtime.Poller.PollUntilDone, which only knows standard Azure LRO states and does not treat superseded as terminal. As a result, WhenUpdatesCompleted will keep polling past a superseded status until the service returns a different state or the context times out — diverging from the .NET behaviour.
The PR acknowledges this in the open design questions, but it is worth flagging explicitly since .NET has already shipped the fix:
// .NET — FoundryMemoryProvider.cs
if (status == MemoryStoreUpdateStatus.Completed || status == MemoryStoreUpdateStatus.Superseded)
return; // treat superseded as successSuggestion: before merging, consider either (a) using a custom PollingHandler / response classifier in the Azure Go SDK so the poller treats superseded as done, or (b) replacing PollUntilDone with a manual polling loop that mirrors .NET's WaitForUpdateAsync and explicitly handles superseded → success.
There was a problem hiding this comment.
Pull request overview
Adds an opt-in mechanism to await completion of Foundry’s asynchronous memory extraction, enabling deterministic sequencing of “store → search” without making the normal store() path blocking.
Changes:
- Retains the most recent Foundry LRO poller returned by
BeginUpdateMemoriesand exposesWhenUpdatesCompleted(ctx, pollingInterval)to drive it to completion. - Keeps
store()non-blocking while allowing callers (notably tests) to wait for extraction to finish before searching. - Adds unit tests covering success (including “clears pending update”), failure, and “no pending update” behavior using the existing recording transport harness.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| provider/foundryprovider/memory.go | Stores the latest update poller and adds WhenUpdatesCompleted to await LRO completion on demand. |
| provider/foundryprovider/memory_test.go | Adds tests validating await-success, await-failure, and no-op behavior when nothing is pending. |
# Conflicts: # provider/foundryprovider/memory_test.go
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by Go API Consistency Review Agent · sonnet46 · 31.6 AIC · ⌖ 5.27 AIC · ⊞ 6K
| if poller == nil { | ||
| return nil | ||
| } | ||
| if _, err := poller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: pollingInterval}); err != nil { |
There was a problem hiding this comment.
Parity gap — superseded terminal state not handled
The .NET WhenUpdatesCompletedAsync (FoundryMemoryProvider.cs) explicitly treats superseded as a success terminal state alongside completed:
if (status == MemoryStoreUpdateStatus.Completed || status == MemoryStoreUpdateStatus.Superseded)
{
return;
}This Go implementation delegates to runtime.Poller.PollUntilDone, which only knows about generic Azure LRO states (Succeeded, Failed, Canceled). The Foundry-specific superseded state is not a standard Azure LRO terminal state, so the poller will likely loop until ctx is cancelled if an update is superseded by a newer one.
The PR description flags this as a known follow-up, but callers awaiting in a test or workflow may hang silently. Consider adding an explicit doc note to the godoc comment warning that a superseded update will block until the context deadline, and linking to a tracking issue.
API Consistency Review — PR #653Scope: This PR adds Upstream parity reference:
Overall: The Go PR correctly ports the concept from .NET. Two parity gaps were found and flagged with inline comments:
Until those two gaps are resolved, the
|
There was a problem hiding this comment.
Generated by Go API Consistency Review Agent · sonnet46 · 30.5 AIC · ⌖ 4.97 AIC · ⊞ 6K
| if poller == nil { | ||
| return nil | ||
| } | ||
| if _, err := poller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: pollingInterval}); err != nil { |
There was a problem hiding this comment.
Parity gap — required vs optional pollingInterval
In the .NET implementation, WhenUpdatesCompletedAsync makes pollingInterval optional with a 5-second default:
public async Task WhenUpdatesCompletedAsync(
TimeSpan? pollingInterval = null, // defaults to 5 seconds
CancellationToken cancellationToken = default)The Go signature requires callers to pass an explicit pollingInterval:
func (p *MemoryProvider) WhenUpdatesCompleted(ctx context.Context, pollingInterval time.Duration) errorThis is a minor ergonomics gap: callers must always choose a value rather than relying on a sensible default. Consider treating a zero pollingInterval as "use the default 5 s interval" to stay aligned with the .NET UX.
| if p.pendingUpdate == poller { | ||
| p.pendingUpdate = nil | ||
| } | ||
| p.mu.Unlock() |
There was a problem hiding this comment.
Parity gap — superseded terminal state not handled
The .NET implementation explicitly treats superseded as a success terminal state:
if (status == MemoryStoreUpdateStatus.Completed || status == MemoryStoreUpdateStatus.Superseded)
{
return;
}The Go implementation delegates polling to runtime.Poller.PollUntilDone from the Azure SDK, which only treats Succeeded / Completed as terminal success. If Foundry ever returns a superseded status for a polled LRO, PollUntilDone may spin indefinitely or return an error rather than treating it as success.
The PR description already flags this as an open question. For parity with .NET, superseded should be treated as a success outcome (the update was replaced by a newer one, so the net effect is the same). Consider wrapping the poller or post-processing the error to handle superseded as a no-op success.
What
Adds
WhenUpdatesCompleted(ctx, pollingInterval)to the FoundryMemoryProviderso callers can await asynchronous memory extraction before searching.store()previously discarded the poller returned byBeginUpdateMemories, making memory updates fire-and-forget with no way to know when they land. This retains the most recent poller on the provider (guarded by async.Mutex) and drives it to completion on demand viaruntime.Poller.PollUntilDone.store()stays non-blocking, so only opt-in callers pay the polling cost.nilwhen no update is pending or the update completed successfully.Why (parity)
Memory extraction on Foundry is a long-running operation. The .NET/Python agent-framework Foundry memory providers submit the update and expose a way to await the extraction result (the LRO poller / update-result endpoint) so a subsequent
SearchMemoriessees the freshly written memories. The Go port kept the fire-and-forget submit but never surfaced the completion handle, so tests and callers had no deterministic way to sequence store -> search. This closes that gap while keeping the hot path unchanged.Not a duplicate of #627/#619/#617.
How tested
go build ./...,go vet ./provider/foundryprovider/...,go test ./provider/foundryprovider/...all pass. New tests inmemory_test.gouse the existingrecordingTransportto:BeginUpdateMemories(202 +Operation-Location) then a status sequencequeued -> completed, assertingWhenUpdatesCompletedreturns nil and clears the pending update (a second call issues no further requests);failedstatus, asserting a non-nil error;Open design questions
pollingIntervaland relies on the caller'sctxfor the overall deadline. Should it instead take a fullruntime.PollUntilDoneOptions, or default the interval?PollUntilDonetreatscompletedas success andfailedas error. Foundry also has asupersededterminal state (an update replaced by a newer one); the generic poller does not treat it as terminal. If awaiting a superseded update matters, a follow-up could pollGetUpdateResultdirectly and treatsupersededas success. Flagging as a follow-up rather than expanding scope here.